package com.nvlad.yii2support.views;
import com.intellij.codeInsight.template.macro.SplitWordsMacro;
import com.intellij.openapi.util.Key;
import com.intellij.psi.PsiDirectory;
import com.intellij.psi.PsiElement;
import com.intellij.psi.PsiFile;
import com.intellij.psi.search.SearchScope;
import com.intellij.psi.util.PsiTreeUtil;
import com.jetbrains.php.PhpIndex;
import com.jetbrains.php.lang.parser.PhpElementTypes;
import com.jetbrains.php.lang.psi.PhpFile;
import com.jetbrains.php.lang.psi.elements.*;
import com.nvlad.yii2support.common.ClassUtils;
import gnu.trove.THashSet;
import org.jetbrains.annotations.NotNull;
import org.jetbrains.annotations.Nullable;
import java.util.*;
/**
* Created by NVlad on 15.01.2017.
*/
public class ViewsUtil {
public static final Key<String> RENDER_VIEW = Key.create("com.yii2support.views.render.view");
public static final Key<PsiFile> RENDER_VIEW_FILE = Key.create("com.yii2support.views.viewFile");
private static final Key<String> RENDER_VIEW_PATH = Key.create("views.viewPath");
private static final Key<Long> VIEW_FILE_MODIFIED = Key.create("com.yii2support.views.viewFileModified");
private static final Key<ArrayList<String>> VIEW_VARIABLES = Key.create("com.yii2support.views.viewVariables");
private static final Key<PsiDirectory> VIEWS_DIRECTORY = Key.create("views.directory");
private static final Key<Long> VIEWS_DIRECTORY_MODIFIED = Key.create("views.directory.modified");
private static final Key<PsiDirectory> VIEWS_CONTEXT_DIRECTORY = Key.create("views.context.directory");
private static final Set<String> ignoredVariables = getIgnoredVariables();
public static final String[] renderMethods = {"render", "renderAjax", "renderPartial"};
private static Set<String> getIgnoredVariables() {
final Set<String> set = new THashSet<>(Arrays.asList("this", "_file_", "_params_"));
set.addAll(Variable.SUPERGLOBALS);
return set;
}
@NotNull
private static ArrayList<String> getPhpViewVariables(PsiFile psiFile) {
final ArrayList<String> result = new ArrayList<>();
final HashSet<String> allVariables = new HashSet<>();
final HashSet<String> declaredVariables = new HashSet<>();
final Collection<Variable> viewVariables = PsiTreeUtil.findChildrenOfType(psiFile, Variable.class);
for (FunctionReference reference : PsiTreeUtil.findChildrenOfType(psiFile, FunctionReference.class)) {
if (reference.getNode().getElementType() == PhpElementTypes.FUNCTION_CALL && psiFile.getUseScope().equals(reference.getUseScope())) {
if (reference.getName() != null && reference.getName().equals("compact")) {
for (PsiElement element : reference.getParameters()) {
if (element instanceof StringLiteralExpression) {
allVariables.add(((StringLiteralExpression) element).getContents());
}
}
}
}
}
final SearchScope fileScope = psiFile.getUseScope();
for (Variable variable : viewVariables) {
String variableName = variable.getName();
if (variable.isDeclaration() && fileScope.equals(variable.getUseScope()) && !(variable.getParent() instanceof PhpUseList)) {
declaredVariables.add(variableName);
} else {
if (!ignoredVariables.contains(variableName)) {
if (fileScope.equals(variable.getUseScope()) || variable.getParent() instanceof PhpUseList) {
if (variable.getName().equals("") && variable.getParent() instanceof StringLiteralExpression) {
Variable inlineVariable = PsiTreeUtil.findChildOfType(variable, Variable.class);
if (inlineVariable != null) {
allVariables.add(inlineVariable.getName());
}
} else {
allVariables.add(variableName);
}
}
}
}
}
for (String variable : allVariables) {
if (!declaredVariables.contains(variable)) {
result.add(variable);
}
}
return result;
}
@NotNull
public static ArrayList<String> getViewVariables(PsiFile psiFile) {
ArrayList<String> result = null;
Long viewModified = psiFile.getUserData(VIEW_FILE_MODIFIED);
if (viewModified != null && psiFile.getModificationStamp() == viewModified) {
result = psiFile.getUserData(VIEW_VARIABLES);
}
if (result == null) {
if (psiFile instanceof PhpFile) {
result = getPhpViewVariables(psiFile);
}
if (result == null) {
result = new ArrayList<>();
}
psiFile.putUserData(VIEW_VARIABLES, result);
psiFile.putUserData(VIEW_FILE_MODIFIED, psiFile.getModificationStamp());
}
return new ArrayList<>(result);
}
@Nullable
public static PsiFile getViewFile(PsiElement element) {
final MethodReference reference = PsiTreeUtil.getParentOfType(element, MethodReference.class);
if (reference == null) {
return null;
}
final PsiElement[] parameters = reference.getParameters();
if (parameters.length == 0 || !(parameters[0] instanceof StringLiteralExpression)) {
return null;
}
String view = reference.getUserData(RENDER_VIEW);
if (!((StringLiteralExpression) parameters[0]).getContents().equals(view)) {
view = ((StringLiteralExpression) parameters[0]).getContents();
reference.putUserData(RENDER_VIEW, view);
reference.putUserData(RENDER_VIEW_FILE, null);
reference.putUserData(RENDER_VIEW_PATH, null);
}
PsiFile file = reference.getUserData(RENDER_VIEW_FILE);
if (file != null && file.isValid()) {
if (!file.getVirtualFile().getPath().equals(reference.getUserData(RENDER_VIEW_PATH))) {
reference.putUserData(RENDER_VIEW_FILE, null);
reference.putUserData(RENDER_VIEW_PATH, null);
file = null;
}
}
if (file == null || !file.isValid()) {
if (reference.getParameters()[0] instanceof StringLiteralExpression) {
PsiDirectory directory;
String path = ((StringLiteralExpression) reference.getParameters()[0]).getContents();
if (path.startsWith("/")) {
directory = ViewsUtil.getRootDirectory(element);
path = path.substring(1);
} else {
directory = ViewsUtil.getContextDirectory(element);
}
String filename;
if (path.contains("/")) {
filename = path.substring(path.lastIndexOf('/') + 1);
path = path.substring(0, path.lastIndexOf('/') + 1);
} else {
filename = path;
path = "";
}
while (path.contains("/") && directory != null) {
final String dirName = path.substring(0, path.indexOf('/'));
directory = dirName.equals("..") ? directory.getParent() : directory.findSubdirectory(dirName);
path = path.substring(path.indexOf('/') + 1);
}
if (directory == null) {
return null;
}
if (filename.contains(".")) {
file = directory.findFile(filename);
} else {
file = directory.findFile(filename + ".php");
if (file == null) {
file = directory.findFile(filename + ".twig");
}
if (file == null) {
file = directory.findFile(filename + ".tpl");
}
}
if (file != null) {
reference.putUserData(RENDER_VIEW_FILE, file);
reference.putUserData(RENDER_VIEW_PATH, file.getVirtualFile().getPath());
}
}
}
return file;
}
public static PsiDirectory getRootDirectory(PsiElement element) {
final PsiFile file = element.getContainingFile();
if (file.getUserData(VIEWS_DIRECTORY) != null) {
Long modified = file.getUserData(VIEWS_DIRECTORY_MODIFIED);
if (modified != null && modified == file.getModificationStamp()) {
return file.getUserData(VIEWS_DIRECTORY);
}
}
return findDirectory(element);
}
public static PsiDirectory getContextDirectory(PsiElement element) {
final PsiFile file = element.getContainingFile();
if (file.getUserData(VIEWS_CONTEXT_DIRECTORY) != null) {
Long modified = file.getUserData(VIEWS_DIRECTORY_MODIFIED);
if (modified != null && modified == file.getModificationStamp()) {
return file.getUserData(VIEWS_CONTEXT_DIRECTORY);
}
}
findDirectory(element);
return file.getUserData(VIEWS_CONTEXT_DIRECTORY);
}
@Nullable
private static PsiDirectory findDirectory(PsiElement element) {
final PhpClass phpClass = PsiTreeUtil.getParentOfType(element, PhpClass.class);
final PsiFile file = element.getContainingFile();
if (phpClass != null) {
return findClassDirectory(phpClass, file);
}
PsiDirectory context = file.getOriginalFile().getContainingDirectory();
if (context != null) {
PsiDirectory root = context.getParentDirectory();
while (root != null && !root.getName().equals("views")) {
root = root.getParentDirectory();
}
file.putUserData(VIEWS_CONTEXT_DIRECTORY, context);
file.putUserData(VIEWS_DIRECTORY, root);
file.putUserData(VIEWS_DIRECTORY_MODIFIED, file.getModificationStamp());
return root;
}
return null;
}
@Nullable
private static PsiDirectory findClassDirectory(PhpClass phpClass, PsiFile file) {
if (phpClass != null) {
Method getViewPath = phpClass.findMethodByName("getViewPath");
PhpClass ownClass = phpClass.getSuperClass();
PsiDirectory directory = file.getOriginalFile().getContainingDirectory();
if (directory != null) {
while (ownClass != null) {
if (getViewPath != null) {
getViewPath = ownClass.findMethodByName("getViewPath");
}
switch (ownClass.getFQN()) {
case "\\yii\\base\\Controller":
directory = directory.getParentDirectory();
if (directory == null) {
return null;
}
if (getViewPath != null && Objects.equals(getViewPath.getContainingClass(), ownClass)) {
directory = directory.findSubdirectory("views");
}
if (directory != null) {
file.putUserData(VIEWS_DIRECTORY_MODIFIED, file.getModificationStamp());
file.putUserData(VIEWS_DIRECTORY, directory);
String controllerId = phpClass.getName();
controllerId = controllerId.substring(0, controllerId.length() - 10);
controllerId = new SplitWordsMacro.LowercaseAndDash().convertString(controllerId);
PsiDirectory context = directory.findSubdirectory(controllerId);
if (context != null) {
file.putUserData(VIEWS_CONTEXT_DIRECTORY, context);
}
}
return directory;
case "\\yii\\base\\Widget":
directory = directory.findSubdirectory("views");
if (directory != null) {
file.putUserData(VIEWS_DIRECTORY_MODIFIED, file.getModificationStamp());
file.putUserData(VIEWS_DIRECTORY, directory);
file.putUserData(VIEWS_CONTEXT_DIRECTORY, directory);
}
return directory;
}
ownClass = ownClass.getSuperClass();
}
}
}
return null;
}
public static boolean isValidRenderMethod(MethodReference methodReference) {
final PhpClass clazz = ClassUtils.getPhpClassByCallChain(methodReference);
if (clazz == null) {
return false;
}
final PhpIndex phpIndex = PhpIndex.getInstance(clazz.getProject());
return ClassUtils.isClassInheritsOrEqual(clazz, "\\yii\\base\\Controller", phpIndex)
|| ClassUtils.isClassInheritsOrEqual(clazz, "\\yii\\base\\View", phpIndex)
|| ClassUtils.isClassInheritsOrEqual(clazz, "\\yii\\base\\Widget", phpIndex)
|| ClassUtils.isClassInheritsOrEqual(clazz, "\\yii\\mail\\BaseMailer", phpIndex);
}
}